package game;

import java.lang.reflect.Array;
import java.util.HashMap;

/**
 * Simple library that allows you to do smart replacement in strings.
 * @author Luan Nico
 */
public final class LScript {
    
    public static final int DEF_DEC = 2;

    private LScript() {
    }
    
    public static String parse(String exp, HashMap<String, Object> vars) throws ParsingError {
        try {
            StringBuilder res = new StringBuilder();
            for (int i = 0; i < exp.length(); i++)
                switch (exp.charAt(i)) {
                    case '#': { //array
                        StringBuilder name = new StringBuilder();
                        char end = '[';
                        if (exp.charAt(++i) == '(') {
                            i++;
                            end = ')';
                        }
                        while (exp.charAt(i) != end)
                            name.append(exp.charAt(i++));
                        Object o = vars.get(name.toString());
                        i++; //character was necessarily [
                        
                        int j = getNext(exp, i, '[', ']');
                        String index = i == j ? "" : exp.substring(i, j);
                        i = j;
                        //String index = "";
                        //while (exp.charAt(i) != ']')
                        //    index += exp.charAt(i++);
                        
                        if (index.isEmpty()) //length
                            res.append(Array.getLength(o));
                        else
                            res.append(Array.get(o, Integer.parseInt(evaluate(index, vars))));
                        break;
                    }
                    case '@': { //control structure
                        String keyword = "";
                        i++;
                        while (exp.charAt(i) != '(')
                            keyword += exp.charAt(i++);
                        i++;

                        int op; //check what kind of structure it is
                        switch (keyword) {
                            case "":
                            case "if":
                                op = 0;
                                break;
                            case "for":
                                op = 1;
                                break;
                            case "e":
                            case "eval":
                                op = 2;
                                break;
                            default:
                                throw new ParsingError("Unknown control structure.");
                        }

                        int j = getNext(exp, i, '(', ')');
                        String name = exp.substring(i, j);
                        i = j;

                        String iftrue = null, iffalse = null;
                        if (op < 2) { //there is iftrue
                            i += 2; //), next character was necessarily {
                            j = getNext(exp, i, '{', '}');
                            iftrue = exp.substring(i, j);
                            i = j;
                            if (op == 0 && ++i < exp.length() && exp.charAt(i) == '{') { //it is a if and there is an iffalse
                                i++;
                                j = getNext(exp, i, '{', '}');
                                iffalse = exp.substring(i, j);
                                i = j;
                            }
                        }
                        if (op == 0 && iffalse == null)
                            i--;

                        switch (op) {
                            case 0:
                                //evaluate first (parsing will be done inside, multilevel)
                                res.append(is(evaluate(name, vars).trim()) ? parse(iftrue, vars) : (iffalse == null ? "" : parse(iffalse, vars)));
                                break;
                            case 1:
                                name = name.trim();
                                if (name.charAt(0) != '%')
                                    throw new ParsingError("Incorrec @for sintax. Should be: @for(%i = 1, 10, 1){something}");
                                char var = name.charAt(1);
                                String[] ps = name.split("[=,]");
                                double current = Double.parseDouble(evaluate(ps[1].trim(), vars));
                                double end = Double.parseDouble(evaluate(ps[2].trim(), vars));
                                double passo = 1;
                                if (ps.length == 4)
                                    passo = Double.parseDouble(evaluate(ps[3].trim(), vars));
                                Object bv = vars.get("%" + var);
                                while (current < end) {
                                    vars.put("%" + var, current);
                                    //evaluate
                                    res.append(evaluate(iftrue, vars));
                                    current += passo;
                                }
                                vars.remove("%" + var);
                                if (bv != null)
                                    vars.put("%" + var, bv);
                                break;
                            case 2:
                                res.append(evaluate(name.trim(), vars));
                                break;
                        }
                        break;
                    }
                    case '$': { //variables
                        String name;
                        boolean s = false;
                        if (exp.charAt(++i) == '(') {
                            int j = getNext(exp, i + 1, '(', ')');
                            name = exp.substring(i + 1, j);
                            i = j;
                        } else {
                            StringBuilder b = new StringBuilder();
                            while (i < exp.length() && exp.charAt(i) != ' ')
                                b.append(exp.charAt(i++));
                            name = b.toString();
                            s = true;
                        }

                        res.append(LScript.format(vars.get(evaluate(name, vars)).toString())).append(s ? " " : "");
                        break;
                    }
                    case '\\': {
                        res.append(exp.charAt(++i)); //skips this and adds next chracter to the sequence, ignoring it
                        break;
                    }
                    default:
                        res.append(exp.charAt(i));
                }
            return res.toString();
        } catch (NullPointerException ex) {
            throw new ParsingError("Variable not found in map.");
        } catch (IndexOutOfBoundsException | NumberFormatException ex) {
            throw new ParsingError(ex.getLocalizedMessage());
        }
    }

    private static int getNext(String t, int s, char c1, char c2) {
        int balance = 0;
        while (s < t.length()) {
            if (t.charAt(s) == c1)
                balance++;
            else if (t.charAt(s) == c2)
                if (balance == 0)
                    break;
                else
                    balance--;
            s++;
        }
        return s;
    }

    private static boolean is(String v) throws ParsingError {
        switch (v) {
            case "true":
            case "1":
                return true;
            case "false":
            case "0":
                return false;
            default:
                throw new ParsingError("Cannot cast " + v + " to boolean.");
        }
    }

    public static class ParsingError extends Exception {
        private static final long serialVersionUID = 1L;

        public ParsingError(String t) {
            super(t);
        }
    }

    public static String evaluate(String v, HashMap<String, Object> vars) throws ParsingError {
        //comparators: %==, %!=, %>, %<, %>=, %<=
        //logic: %&, %|, %^
        //order: %(, %)
        //aritmetic: %+, %-, %*, %/
        //string operations: %.
        //escape: %%
        StringBuilder id1 = new StringBuilder();
        String id2 = null, op = null;
        for (int i = 0; i < v.length(); i++)
            if (v.charAt(i) != '%')
                id1.append(v.charAt(i));
            else {
                char c = v.charAt(++i);
                if (c >= 'a' && c <= 'z') //iterator
                    id1.append(format(vars.get("%" + c).toString()));
                else
                    switch (c) {
                        case '%': { //escape
                            id1.append("%");
                            break;
                        }
                        case '(': {
                            //find the end of this parentesis
                            int j = getNext(v, i + 1, '(', ')');
                            id1.append(evaluate(v.substring(i + 1, j), vars));
                            i = j;
                            break;
                        }
                        case '^': {
                            char end = ' ';
                            if (v.charAt(++i) == '(')
                                end = ')';
                            int j = i;
                            while (v.charAt(++j) != end);
                            String t = v.substring(i + 1, j - 1);
                            if (end == ')') //var
                                t = evaluate(t, vars);
                            id1.append(String.valueOf(!is(parse(t, vars))));
                            break;
                        }
                        case '!':
                        case '=':
                        case '<':
                        case '>': {
                            //comparator
                            String i1 = id1.toString();
                            if (id2 == null)
                                id2 = i1;
                            else //evaluate first part then proceed
                                id2 = calculate(id2, i1, op, vars);
                            op = v.substring(i, i + 2);
                            if (op.charAt(1) != '=')
                                op = op.substring(0, 1);
                            else
                                i++;
                            id1 = new StringBuilder();
                            break;
                        }
                        default: //other one-caracther operators
                            String i1 = id1.toString();
                            if (id2 == null)
                                id2 = i1;
                            else //evaluate first part then proceed
                                id2 = calculate(id2, i1, op, vars);
                            op = String.valueOf(v.charAt(i));
                            id1 = new StringBuilder();
                            break;
                    }
            }
        return calculate(id2, id1.toString(), op, vars);
    }
    
    public static String format(double d) {
            if (d == Math.round(d))
                return String.valueOf((int) d);
            return String.valueOf(d);        
    }
    
    public static String format(double d, int dec) {
            if (d == Math.round(d))
                return String.valueOf((int) d);
            double e = Math.pow(10, dec);
            return String.valueOf(Math.round(d * e)/e);
    }
    
    public static String format(String t) {
        return format(t, DEF_DEC);
    }

    public static String format(String t, int dec) {
        try {
            return format(Double.parseDouble(t), dec);
        } catch (NumberFormatException ex) {
            return t;
        }
    }

    private static String calculate(String id1, String id2, String op, HashMap<String, Object> vars) throws ParsingError {
        id2 = format(parse(id2, vars));
        if (id1 == null)
            return id2;
        id1 = format(parse(id1, vars)); //makes numbers fancier
        try {
            switch (op) {
                case "==":
                    return (id1.trim().equals(id2.trim()) ? "true" : "false"); //it works for strings and ints
                case "!=":
                    return (id1.trim().equals(id2.trim()) ? "false" : "true");
                case ">=":
                    return (id1.trim().compareTo(id2.trim()) >= 0 ? "true" : "false");
                case "<=":
                    return (id1.trim().compareTo(id2.trim()) <= 0 ? "true" : "false");
                case ">":
                    return (id1.trim().compareTo(id2.trim()) > 0 ? "true" : "false");
                case "<":
                    return (id1.trim().compareTo(id2.trim()) < 0 ? "true" : "false");
                case "+":
                    return format(String.valueOf(Double.parseDouble(id1) + Double.parseDouble(id2)));
                case "-":
                    return format(String.valueOf(Double.parseDouble(id1) - Double.parseDouble(id2)));
                case "*":
                    return format(String.valueOf(Double.parseDouble(id1) * Double.parseDouble(id2)));
                case "/":
                    return format(String.valueOf(Double.parseDouble(id1) / Double.parseDouble(id2)));
                case ".":
                    return id1 + id2;
                case "&":
                    return String.valueOf(is(id1.trim()) && is(id2.trim()));
                case "|":
                    return String.valueOf(is(id1.trim()) || is(id2.trim()));
                default:
                    throw new ParsingError("Unknown operator on evaluation: " + op);
            }
        } catch (NumberFormatException | ParsingError ex) {
            throw new ParsingError(ex.getMessage());
        }
    }
}
